<?php

/**
 * Class SettingsContainerAbstract
 *
 * @created      28.08.2018
 *
 * @author       Smiley <smiley@chillerlan.net>
 * @copyright    2018 Smiley
 * @license      MIT
 */
declare(strict_types=1);

namespace zxf\QrCode\Settings;

use const JSON_THROW_ON_ERROR;

use InvalidArgumentException;
use JsonException;
use ReflectionClass;
use ReflectionProperty;

use function array_keys;
use function get_object_vars;
use function is_object;
use function json_decode;
use function json_encode;
use function json_last_error_msg;
use function method_exists;
use function property_exists;
use function serialize;
use function unserialize;

abstract class SettingsContainerAbstract implements SettingsContainerInterface
{
    /**
     * SettingsContainerAbstract constructor.
     *
     * @phpstan-param array<string, mixed> $properties
     */
    public function __construct(?iterable $properties = null)
    {

        if (! empty($properties)) {
            $this->fromIterable($properties);
        }

        $this->construct();
    }

    /**
     * calls a method with trait name as replacement constructor for each used trait
     * (remember pre-php5 classname constructors? yeah, basically this.)
     */
    protected function construct(): void
    {
        $traits = (new ReflectionClass($this))->getTraits();

        foreach ($traits as $trait) {
            $method = $trait->getShortName();

            if (method_exists($this, $method)) {
                $this->{$method}();
            }
        }

    }

    /**
     * {@inheritdoc}
     */
    public function __get(string $property): mixed
    {

        if (! property_exists($this, $property) || $this->isPrivate($property)) {
            return null;
        }

        $method = 'get_'.$property;

        if (method_exists($this, $method)) {
            return $this->{$method}();
        }

        return $this->{$property};
    }

    /**
     * {@inheritdoc}
     */
    public function __set(string $property, mixed $value): void
    {

        if (! property_exists($this, $property) || $this->isPrivate($property)) {
            return;
        }

        $method = 'set_'.$property;

        if (method_exists($this, $method)) {
            $this->{$method}($value);

            return;
        }

        $this->{$property} = $value;
    }

    /**
     * {@inheritdoc}
     */
    public function __isset(string $property): bool
    {
        return isset($this->{$property}) && ! $this->isPrivate($property);
    }

    /**
     * @internal Checks if a property is private
     */
    protected function isPrivate(string $property): bool
    {
        return (new ReflectionProperty($this, $property))->isPrivate();
    }

    /**
     * {@inheritdoc}
     */
    public function __unset(string $property): void
    {

        if ($this->__isset($property)) {
            unset($this->{$property});
        }

    }

    /**
     * {@inheritdoc}
     */
    public function __toString(): string
    {
        return $this->toJSON();
    }

    /**
     * {@inheritdoc}
     */
    public function toArray(): array
    {
        $properties = [];

        foreach (array_keys(get_object_vars($this)) as $key) {
            $properties[$key] = $this->__get($key);
        }

        return $properties;
    }

    /**
     * {@inheritdoc}
     */
    public function fromIterable(iterable $properties): static
    {

        foreach ($properties as $key => $value) {
            $this->__set($key, $value);
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function toJSON(?int $jsonOptions = null): string
    {
        $json = json_encode($this, ($jsonOptions ?? 0));

        if ($json === false) {
            throw new JsonException(json_last_error_msg());
        }

        return $json;
    }

    /**
     * {@inheritdoc}
     */
    public function fromJSON(string $json): static
    {
        /** @phpstan-var array<string, mixed> $data */
        $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

        return $this->fromIterable($data);
    }

    /**
     * {@inheritdoc}
     *
     * @return array<string, mixed>
     */
    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    /**
     * Returns a serialized string representation of the object in its current state (except static/readonly properties)
     *
     * {@inheritdoc}
     *
     * @see \zxf\QrCode\Settings\SettingsContainerInterface::toArray()
     */
    public function serialize(): string
    {
        return serialize($this);
    }

    /**
     * Restores the data (except static/readonly properties) from the given serialized object to the current instance
     *
     * {@inheritdoc}
     *
     * @see \zxf\QrCode\Settings\SettingsContainerInterface::fromIterable()
     */
    public function unserialize(string $data): void
    {
        $obj = unserialize($data);

        if ($obj === false || ! is_object($obj)) {
            throw new InvalidArgumentException('The given serialized string is invalid');
        }

        $reflection = new ReflectionClass($obj);

        if (! $reflection->isInstance($this)) {
            throw new InvalidArgumentException('The unserialized object does not match the class of this container');
        }

        $properties = $reflection->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY));

        foreach ($properties as $reflectionProperty) {
            $this->{$reflectionProperty->name} = $reflectionProperty->getValue($obj);
        }

    }

    /**
     * Returns a serialized string representation of the object in its current state (except static/readonly properties)
     *
     * {@inheritdoc}
     *
     * @see \zxf\QrCode\Settings\SettingsContainerInterface::toArray()
     */
    public function __serialize(): array
    {

        $properties = (new ReflectionClass($this))
            ->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY));

        $data = [];

        foreach ($properties as $reflectionProperty) {
            $data[$reflectionProperty->name] = $reflectionProperty->getValue($this);
        }

        return $data;
    }

    /**
     * Restores the data from the given array to the current instance
     *
     * {@inheritdoc}
     *
     * @see \zxf\QrCode\Settings\SettingsContainerInterface::fromIterable()
     *
     * @param  array<string, mixed>  $data
     */
    public function __unserialize(array $data): void
    {

        foreach ($data as $key => $value) {
            $this->{$key} = $value;
        }

    }
}
